﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Reflection;
using System.Web;
using System.Web.UI;
using System.Linq;
using Castle.MonoRail.Framework.Helpers;

namespace Hadlow.Jobvertizer.HtmlHelpers
{
    public class HierarchicalSelect : AbstractHelper
    {
        const string indentation = "&nbsp;&nbsp;&nbsp;&nbsp;";

        // NVelocity doesn't support generic methods :(
        // we have to take an object argument and then construct one of the generic methods first
        // before calling it.
        public string WriteHtml(string target, object source, object item)
        {
            Type sourceType = source.GetType();
            
            // get our unambiguous generic method
            MethodInfo writeHtmlMethod = typeof(HierarchicalSelect).GetMethod("WriteHtmlUnambiguous", 
                BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance);

            // create a typed version
            MethodInfo typedWriteHtmlMethod = writeHtmlMethod.MakeGenericMethod(sourceType);
            
            // invoke it.
            return (string)typedWriteHtmlMethod.Invoke(this, new object[] { target, source, item });
        }

        public string WriteHtml(string target, object source)
        {
            return WriteHtml(target, source, null);
        }

        // need to provide this so that GetMethod can unambiguously find it.
        private string WriteHtmlUnambiguous<T>(string target, T source, object item)
        {
            return WriteHtml<T>(target, source, item);
        }

        public string WriteHtml<T>(string target, T source)
        {
            return WriteHtml<T>(target, source, null);
        }

        public string WriteHtml<T>(string target, T source, object item)
        {
            return WriteHtml<T>(target, source, item, null);
        }

        public string WriteHtml<T>(string target, T source, object item, IDictionary attributes)
        {
            string id = CreateHtmlId(target);
            object selectedId = new TokenWalker(item, target).GetValue();
            string name = target;

            StringBuilder sb = new StringBuilder();
            StringWriter sbWriter = new StringWriter(sb);
            HtmlTextWriter writer = new HtmlTextWriter(sbWriter);

            writer.WriteBeginTag("select");
            writer.WriteAttribute("id", id);
            writer.WriteAttribute("name", name);
            writer.Write(" ");
            writer.Write(GetAttributes(attributes));
            writer.Write(HtmlTextWriter.TagRightChar);
            writer.WriteLine();

            if (source != null)
            {
                HierarchicalThing<T> root = new HierarchicalThing<T>(selectedId, source);
                WriteOptions(writer, root, 0);
            }

            writer.WriteEndTag("select");

            return sbWriter.ToString();
        }

        private void WriteOptions<T>(
            HtmlTextWriter writer, 
            HierarchicalThing<T> item, 
            int level)
        {
            writer.WriteBeginTag("option");

            if (item.IsSelected)
            {
                writer.Write(" selected=\"selected\"");
            }

            writer.WriteAttribute("value", HttpUtility.HtmlEncode(item.Id));
            writer.Write(HtmlTextWriter.TagRightChar);
            writer.Write(GetLevelString(level) + HttpUtility.HtmlEncode(item.ToString()));
            writer.WriteEndTag("option");
            writer.WriteLine();

            level++;
            foreach (HierarchicalThing<T> child in item.Children)
            {
                WriteOptions(writer, child, level);
            }
        }

        private string GetLevelString(int level)
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < level; i++)
            {
                sb.Append(indentation);
            }
            return sb.ToString();
        }

        private string CreateHtmlId(string name)
        {
            StringBuilder sb = new StringBuilder(name.Length);

            bool canUseUnderline = false;

            foreach (char c in name.ToCharArray())
            {
                switch (c)
                {
                    case '.':
                    case '[':
                    case ']':
                        if (canUseUnderline)
                        {
                            sb.Append('_');
                            canUseUnderline = false;
                        }
                        break;
                    default:
                        canUseUnderline = true;
                        sb.Append(c);
                        break;
                }

            }

            return sb.ToString();
        }

        /// <summary>
        /// Represents a hierarchial object with one property that has a type of 
        /// IList&lt;sametype&gt;
        /// </summary>
        /// <typeparam name="T"></typeparam>
        private class HierarchicalThing<T>
        {
            readonly T source;
            readonly PropertyInfo childrenProperty;
            readonly string id;
            readonly object selectedId;

            public string Id
            {
                get { return id; }
            }

            public HierarchicalThing(object selectedId, T source)
            {
                this.selectedId = selectedId;
                this.source = source;
                Type sourceType = source.GetType();

                childrenProperty = FindChildProperty(sourceType);
                id = FindIdValue(source);

                if (childrenProperty == null)
                {
                    throw new ApplicationException("The source object must have a property that " +
                        "represents it's children. This property much have a type of IList<source type>. " +
                        "No such property was found in type: " + sourceType.Name);
                }
            }

            private string FindIdValue(T source)
            {
                return source.GetType().GetProperties().First(p =>
                    p.GetCustomAttributes(true).Any(a =>
                        a is Castle.ActiveRecord.PrimaryKeyAttribute)
                        ).GetValue(source, null).ToString();
            }

            private PropertyInfo FindChildProperty(Type sourceType)
            {
                return sourceType.GetProperties().First(p => p.PropertyType == typeof(IList<T>));
            }

            public HierarchicalThing<T>[] Children
            {
                get
                {
                    List<HierarchicalThing<T>> children = new List<HierarchicalThing<T>>();
                    IList<T> childValues = (IList<T>)childrenProperty.GetValue(source, null);
                    foreach (T childValue in childValues)
                    {
                        children.Add(new HierarchicalThing<T>(selectedId, childValue));
                    }
                    return children.ToArray();
                }
            }

            public bool IsSelected
            {
                get
                {
                    if (selectedId == null) return false;
                    return selectedId.ToString() == id;
                }
            }

            public override string ToString()
            {
                return source.ToString();
            }
        }
    }
}
